測試環境
Laravel 為測試提供了一個獨立的環境,預設配置使用 SQLite 記憶體資料庫以便於快速測試,透過 .env.testing 檔案配置測試環境。
測試類
Laravel 使用 PHPUnit 作為測試框架。測試類別位於 tests 目錄中,通常分為 Feature 和 Unit 兩類:
Feature Tests:測試應用的功能,通常涉及多個方法或組件,所以多半用在 testController。Unit Tests:測試單一功能或方法,通常是單一類別或函數的測試。
創建測試
使用 Artisan 命令 php artisan make:test ExampleTest 產生測試類別,也可以使用 php artisan make:test ExampleTest --unit 選項建立單元測試。
運行測試
使用 PHPUnit 指令 php vendor/bin/phpunit 執行所有測試。
如果要執行單一方法測試,可以直接按下方法旁邊的綠色開始 icon 就會執行,並且顯示執行結果!
在測試中,assertion 斷言是一個檢查點,它會比較預期結果和實際結果,並確定它們是否匹配,這是測試框架用來驗證程式碼正確性的核心功能。
如果預期結果和實際結果不一致,斷言將會失敗,並報告錯誤。
紀錄目前有用到的斷言方法:
| 測試對象 | 斷言方法 | 範例 | 
|---|---|---|
| HTTP | actingAs() | $response = $this->actingAs($user)->get('/dashboard');模擬一個已認證的用戶,在測試中可以執行需要身份驗證的操作 | 
| HTTP | assertOk() | $response->assertOk();檢查 HTTP 回應的狀態碼是否為 200 | 
| HTTP | assertUnprocessable | $response->assertUnprocessable();檢查 HTTP 回應的狀態碼是否為 422,通常用於驗證失敗的情況 | 
| HTTP | assertJsonStructure |  檢查 JSON 回應的結構,確保回應中包含特定的字段 | 
| HTTP | assertStatus(int $status) | $response->assertStatus(200);用來確認回應的狀態碼是 200 | 
| HTTP | assertSee(string $text) | $response->assertSee('Welcome');用來檢查回應中是否包含 "Welcome" 字樣 | 
| HTTP | assertJson(array $data) | $response->assertJson(['name' => 'John']); 用來確認 JSON 回應中是否包含 'name' => 'John' | 
| HTTP | assertJsonCount(int $count) | $response->assertJsonCount(3);用來檢查 JSON 資料中是否有 3 個項目 | 
| 資料庫 | assertDatabaseHas(string $table, array $data) | $this->assertDatabaseHas('posts', ['title' => 'New Post']);用來檢查posts表格中是否有 title 為 New Post 的記錄 | 
| 資料庫 | assertDatabaseMissing(string $table, array $data) | $this->assertDatabaseMissing('posts', ['title' => 'Old Post']);用來檢查posts表格中是否沒有 title 為 Old Post 的記錄 | 
參考文章:tests-官方文件原子化翻譯筆記
範例說明
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
   public function testUserCanRegister()
   {
       // 測試案例
       $response = $this->post('/register', [
           'name' => 'kuku Ku',
           'email' => 'kuku@example.com',
           'password' => 'aa123',
       ]);
       // 確認 HTTP 回應狀態碼為 200,表示創建了新資源
       $response->assertStatus(200);
       
       // 確認 JSON 回應中包含 'message' => 'User created successfully!', 確認 API 正確回傳預期的回應
       $response->assertJson([
           'message' => 'User created successfully!',
       ]);
       // 確認 'users' 表格中存在 'email' 為 'kuku@example.com' 的記錄,確保註冊流程將資料正確地寫入資料庫
       $this->assertDatabaseHas('users', [
           'email' => 'kuku@example.com',
       ]);
   }
}
文件:数据库测试
工廠 Factories: 用於生成模型的假資料。
Seeder: 用於填充資料庫的初始資料。
Migration: 在測試前自動設置資料庫結構。
🔔 工廠建立
曾經以為工廠是 laravel 已經做好,只需要用模型搭配內建的工廠就可以建立測試數據,後來發現原來我以為的內建的工廠其實是我的指導者已經把工廠做好,所以我在寫測試過程不用再建立,回家自己練習還是需要自己練習建立的!🤣
話說,文件歸類在數據庫測試,但實際上我呼叫最多次的時機除了在 repository 以外還有 service,讓我們開始吧!
創建工廠
指令 php artisan make:factory TaskFactory --model={模型名稱} 生成一個位於 database/factories 的 {模型名稱}Factory.php 文件就可以在這個工廠中定義{模型名稱}的假數據。
編輯工廠
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
 * 生成 User 模型實例的工廠
 * 
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * 定義模型的預設狀態
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'name' => fake()->name(),  // 使用 fake() 生成隨機的使用者姓名
            'email' => fake()->unique()->safeEmail(),  // 生成一個唯一且安全的電子郵件地址
            'email_verified_at' => now(),  // 設置現在時間,表示電子郵件已驗證
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',  // password
            'remember_token' => Str::random(10),  // 生成一個隨機的 remember_token,用在記住使用者的登入狀態
        ];
    }
    /**
     * 模型的電子郵件地址顯示為未驗證
     *
     * @return static
     */
    public function unverified()
    {
        return $this->state(fn(array $attributes) => [
            'email_verified_at' => null,  // 直接賦值給 null,表示該電子郵件未經過驗證
        ]);
    }
}
模型關聯 - 1 對多
在資料庫中,模型之間的關係可以使用工廠生成。例如,一個 User 模型和一個 Post 模型,並且每個用戶可以有多篇文章。
以下關聯可以回顧第 5 天:數據模型與遷移 - 每日任務 - 一對多
public function posts()
{
    return $this->hasMany(Post::class);
}
public function user()
{
    return $this->belongsTo(User::class);
}
工廠怎麼辦?因為模型已經關聯,所以在用工廠建立資料的時候,就會啟動關聯,在創建與父模型 User 相關的子模型 Post,可以在創建父模型時可以同時創建相關的子模型。
use App\Models\User;
$user = User::factory()
    ->hasPosts(3) // 創建 3 篇文章與 user_id 相關的 Post 實例,彼此間就有關聯
    ->create();
Seeder
可以回顧一下第 6 天:數據庫操作基礎 - 數據庫種子(Database: Seeding)
Seeder 用於填充資料庫,通常用於初始化應用的資料,依照文件說明也可以創建一個 seeder 類並使用工廠填充數據。
<?php
namespace Tests\Feature;
use Database\Seeders\OrderStatusSeeder;
use Database\Seeders\TransactionStatusSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
    // 在每個測試運行前重置資料庫,以確保每次測試都從一個乾淨的狀態開始
    use RefreshDatabase;
    /**
     * 測試建立新訂單
     *
     * @return void
     */
    public function test_orders_can_be_created()
    {
        // 使用 $this->seed() 方法運行默認的 DatabaseSeeder
        $this->seed();
        // 運行特定的 Seeder,這裡是指運行 OrderStatusSeeder
        $this->seed(OrderStatusSeeder::class);
        // ...
        // 運行多個 Seeder,要用陣列包裝起來
        $this->seed([
            OrderStatusSeeder::class,
            TransactionStatusSeeder::class,
            // ...
        ]);
    }
}
刷新資料庫
使用 RefreshDatabase trait,可以在每個測試方法運行前後自動遷移資料庫:
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
    use RefreshDatabase;
    // 測試程式碼...
}
資料庫斷言
使用 assertDatabaseHas 和 assertDatabaseMissing 方法驗證資料庫中的資料:
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
$this->assertDatabaseMissing('users', ['email' => 'missing@example.com']);
文件:测试模拟器 Mocking
學習過程翻最多次的文章:Laravel - Testing - Mocking (官方文件原子化翻譯)
模擬 Mocking
使用 Laravel 的模擬功能建立假數據或模擬服務,看文件說明主要是用在依賴注入的情況,要測試一個類而不依賴它的具體實現或外部服務時:
use Tests\TestCase;
use App\Services\PaymentService;
use Mockery;
class OrderTest extends TestCase
{
    public function testPaymentIsProcessed()
    {
        // 創建一個模擬物件
        $paymentService = Mockery::mock(PaymentService::class);
        
        /** 
         * 指定模擬物件應該接收 process 方法的調用
         * process 方法應該被調用一次
         * 指定調用時應該傳入的參數為 100
         * 當 process 方法被調用時,返回 true
         */
        $paymentService->shouldReceive('process')
            ->once()
            ->with(100)
            ->andReturn(true);
        // 剛剛創建的模擬物件註冊到 Laravel 的服務容器中
        $this->app->instance(PaymentService::class, $paymentService);
        // 發送 HTTP 請求
        $response = $this->post('/orders', ['amount' => 100]);
        // 斷言回應狀態碼
        $response->assertStatus(201);
    }
}
假數據 Faking
使用 Faker 直接產生假資料:
use Tests\TestCase;
use App\Models\User;
class UserTest extends TestCase
{
    public function testUserCreation()
    {
        // 使用工廠創建一個新的用戶實例,產生的假數據存儲到資料庫中
        $user = User::factory()->create([
            'name' => 'kuku',
            'email' => 'kuku@123.com',
        ]);
        // 斷言資料庫狀態
        $this->assertDatabaseHas('users', [
            'email' => 'kuku@123.com',
        ]);
    }
}
測試 API 路由
使用 HTTP 請求測試 API 端點:
$response = $this->getJson('/api/user');
$response->assertStatus(200);
模擬 HTTP 請求
使用 Laravel 的測試工具模擬 HTTP 請求和回應,確保 API 傳回正確的資料:
$response = $this->postJson('/api/user', ['name' => 'kuku']);
$response->assertStatus(201)->assertJson(['name' => 'kuku']);
這是我個人自己定義寫測試的流程,主要是因為身陷其中很容易出現不知道自己到底要做什麼,尤其是開發過程因為要思考程式碼,很容易迷失自己,遺漏需求,所以先寫測試讓自己清楚要做什麼,定義好格式架構,可以讓自己在迷失的時候有燈塔照明前進!
